let fs = require('fs')
let url = require('url')
let path = require('path')
let http = require('http')
let promisify = require('util').promisify

let mime = require('mime-types')

let exists = promisify(fs.exists)
let lstat = promisify(fs.lstat)
let realpath = promisify(fs.realpath)
let readdir = promisify(fs.readdir)
let readFile = promisify(fs.readFile)

let cwd = process.cwd()

main()

function main() {
  let options = {
    vd: '/static',
    // root: '/public'
  }

  http.createServer(static(options))
    .listen({ port: 4900 })
}

/**
 * @description
 * static-server
 * @steps
 * 1 url-resolver
 * 2 fs-reader
 * 3 renderer
 *
 * Reqeust url:string => resolver pathname:string => reader FileInfo:obj => transfomer@transform renderableData => renderer => html|buffer
 */
function static(options) {
  return async (req, res) => {
    let { vd } = options

    let { pathname, isContainVenderName } = resolver.removeBasePath(vd, req.url)

    // result 不是可以直接渲染的
    let result = await reader.read(pathname)

    let renderableData = await transformer.transform(result, {
      pathname,
      isContainVenderName,
      vd
    })

    let data = renderer.render(renderableData, { pathname })

    if (result.data.mime) {
      res.setHeader('Content-Type', result.data.mime)
    }

    res.end(data)
  }
}

let resolver = {
  removeBasePath: (vd, reqUrl) => {
    let { pathname } = url.parse(reqUrl)

    // 处理包含中文路径的情况
    pathname = decodeURIComponent(pathname)

    let isContainVenderName = pathname.startsWith(vd)

    if (isContainVenderName) {
      pathname = pathname.replace(vd, '')
    }

    return { pathname, isContainVenderName }
  }
}

// fs-reader
let reader = {
  /**
   * @return { type:string, data: object }
   */
  read: async pathname => {
    let filename = path.join(cwd, pathname)

    let { isDirectory, isFile, isExist } = await readFileInfo(filename)

    let type = !isExist
      ? '404'
      : isDirectory
      ? 'directory'
      : isFile
      ? 'file'
      : ''

    let data = {
      mime: 'text/html; charset=utf-8'
    }

    if (isDirectory) {
      let files = await readdir(filename)

      data.files = files
    } else if (isFile) {
      data.mime = mime.contentType(path.basename(filename))

      data.content = await readFile(filename)
    }

    return {
      type,
      data
    }
  }
}

const transformer = {
  transform: async (result, options = {}) => {
    // 直接改了原对象，深拷贝 JSON.parse(JSON.stringify) ?
    let { pathname, vd, isContainVenderName } = options

    let { type, data = {} } = result
    let { files } = data

    if (type === '404') {
      result.data.content = 'Page 404'
    } else if (type === 'directory') {
      let { fileMapList, breadCrumbList } = await prepareRenderDirectoryData({
        pathname,
        files,
        vd,
        isContainVenderName
      })

      result.data.fileMapList = fileMapList
      result.data.breadCrumbList = breadCrumbList
    }

    return result
  }
}

let renderer = {
  /**
   * @return 可被 NodeJs 直接渲染的数据
   */
  render: ({ type, data = {} }, { pathname } = {}) => {
    if (type === 'directory') {
      let { fileMapList, breadCrumbList } = data

      let html = renderFiles({ fileMapList, breadCrumbList, title: pathname })
      
      return html
    }
    return data.content
  }
}

let mapValue = (value, index, array) => {
  let newValue = value

  for (let i = index - 1; i >= 0; i--) {
    newValue = array[i] + newValue
  }

  return { href: newValue, value: value.slice(1) }
}

function createBreadCrumbList({ pathname, isContainVenderName, vd }) {
  let breadCrumbList = pathname
    .split('/')
    .filter(Boolean)
    .map(_ => '/' + _)
    .map(mapValue)

  if (isContainVenderName) {
    breadCrumbList = breadCrumbList.map(item => ({
      ...item,
      href: path.join(vd, item.href)
    }))
  }

  breadCrumbList = breadCrumbList.map((item, index, array) => {
    if (index === array.length - 1) {
      return {
        ...item,
        href: 'javascript:;'
      }
    }

    return item
  })

  let indexHref = isContainVenderName ? path.join(vd, '/') : '/'

  if (['', '/'].includes(pathname)) {
    indexHref = 'javascript:;'
  }

  breadCrumbList.unshift({ href: indexHref, value: '~' })

  return breadCrumbList
}

function renderFiles({ fileMapList = [], breadCrumbList = [], title }) {
  if (!fileMapList.length && !breadCrumbList.length) {
    return render('')
  }

  let list = fileMapList.map(({ name, clzName, href }) => {
    return `
      <li>
        <a
          class="${clzName}"
          href="${href}"
        >
          <span
            class="name"
          >
            ${name}
          </span>
        </a>
      </li>
    `
  })

  let navHtml = ''
  if (breadCrumbList.length) {
    navHtml = `
      <h1>
        ${breadCrumbList
          .map(item => {
            let { href, value } = item
            return `<a href="${href}">${value}</a> / `
          })
          .join('')}
      </h1>
    `
  }

  let html = `
    <div>
      ${navHtml}
      <ul id="files">
        ${list.join('')}
      </ul>
    </div>
  `

  return render(html, { title })
}

async function readFileInfo(filename) {
  let isExist = await exists(filename)

  if (!isExist) return { isExist }

  let fileInfo = await getFileInfo(filename)

  return {
    isExist,
    ...fileInfo
  }
}

async function getFileInfo(filename) {
  let stat = await lstat(filename)
  let isFile = stat.isFile()
  let isDirectory = stat.isDirectory()

  if (stat.isSymbolicLink()) {
    let realFilename = await realpath(filename)
    return getFileInfo(realFilename)
  }

  return {
    isFile,
    isDirectory
  }
}

async function addClzNameProp(item) {
  let { relativePath } = item
  let filename = path.join(cwd, relativePath)
  let { isFile, isDirectory } = await getFileInfo(filename)

  let clzName = 'icon'

  if (isDirectory) {
    clzName += ' icon-directory'
  } else if (isFile) {
    let contentType = mime.lookup(relativePath)

    if (contentType) {
      clzName += ' icon-' + contentType.replace(/[.|/]/g, '-')
    } else {
      clzName += ' icon-default'
    }
  }

  return {
    ...item,
    clzName
  }
}

function render(content, { title = '' } = {}) {
  return `
  <html>
    <head>
      <meta charset="utf-8" />
      <meta
        name="viewport"
        content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
      />
      <title>directory ${title}</title>
      <link rel="stylesheet" href="/src/assets/style.css" />
      <link rel="stylesheet" href="/src/assets/icon.css" />
    </head>
    <body class="directory">
      ${content}
    </body>
  </html>
  `
}

async function prepareRenderDirectoryData({
  pathname,
  files,
  isContainVenderName,
  vd
}) {
  // 增加返回上一级目录
  if (pathname !== '/' && pathname !== '') {
    files.unshift('..')
  }

  let fileMapList = files
    // 数组 -> 数组对象「{name, relativePath}」
    .map(name => ({ name, relativePath: path.join(pathname, name) }))
    // 扩充 href 属性
    .map(item => {
      let { relativePath } = item
      return {
        ...item,
        href: !isContainVenderName ? relativePath : path.join(vd, relativePath)
      }
    })

  fileMapList = await Promise.all(fileMapList.map(addClzNameProp))

  let breadCrumbList = createBreadCrumbList({
    pathname,
    isContainVenderName,
    vd
  })

  return {
    fileMapList,
    breadCrumbList
  }
}
